【译】用Java,Spring Boot,Git Flow,Jenkins和Docker构建微服务和DevOps

在这篇文章中,我们使用Java和Spring Boot框架开发了一个微服务,然后使用DevOps管道将它与Jenkins和Docker一起部署。

序言

在本文中,将使用 Java 和 Spring 框架创建一个简单的微服务,并使用Jenkins和Docker创建一个DevOps管道。

注意:假设读者具有 Java 和 Web 技术的背景。本文不再单独介绍Spring,Jenkins,Java,Git和Docker的。

将按顺序介绍以下几点:

  • 构建中的微服务
  • 所需要的软件
  • 使用 Jenkins 和 Docker 构建DevOps管道

微服务

可以使用以下URL从Github克隆微服务应用程序:
https://github.com/Microservices-DevOps/person.git

资源层

实体为Person,包含名称,电子邮件和id。开发一个管理Person实体的微服务。

package com.myapp.sample.model;

import java.io.Serializable;

import javax.persistence.Column;

import javax.persistence.Entity;

import javax.persistence.GeneratedValue;

import javax.persistence.GenerationType;

import javax.persistence.Id;

import javax.persistence.Table;

import javax.validation.constraints.Email;

import javax.validation.constraints.NotNull;

@Entity

@Table(name = "person")

public class Person implements Serializable{

private static final long serialVersionUID = 7401548380514451401L;

public Person() {}

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

Long id;

@Column(name = "name")

String name;

@NotNull

@Email

@Column(name = "email")

String email;

public Long getId() {

return id;

}

public void setId(Long id) {

this.id = id;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getEmail() {

return email;

}

public void setEmail(String email) {

this.email = email;

}

@Override

public int hashCode() {

final int prime = 31;

int result = 1;

result = prime * result + ((email == null) ? 0 : email.hashCode());

result = prime * result + ((id == null) ? 0 : id.hashCode());

result = prime * result + ((name == null) ? 0 : name.hashCode());

return result;

}

@Override

public boolean equals(Object obj) {

if (this == obj)

return true;

if (obj == null)

return false;

if (getClass() != obj.getClass())

return false;

Person other = (Person) obj;

if (email == null) {

if (other.email != null)

return false;

} else if (!email.equals(other.email))

return false;

if (id == null) {

if (other.id != null)

return false;

} else if (!id.equals(other.id))

return false;

if (name == null) {

if (other.name != null)

return false;

} else if (!name.equals(other.name))

return false;

return true;

}

@Override

public String toString() {

return "Person [id=" + id + ", name=" + name + ", email=" + email + "]";

}

}

使用常规CRUD操作测试实体层。然后,我们检查实体是否被持久化,查询和更新

package com.myapp.sample.model;

import org.junit.Assert;

import org.junit.Before;

import org.junit.Test;

import org.junit.runner.RunWith;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;

import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

@RunWith(SpringRunner.class)

@DataJpaTest

public class PersonTest {

@Autowired

private TestEntityManager entityManager;

@Before

public void setUp() {

List<Person> list = entityManager.getEntityManager().createQuery("from Person").getResultList();

for(Person person:list) {

entityManager.remove(person);

}

}

@Test

public void testCRUD()

{

Person p1 = new Person();

p1.setName("test person 1");

p1.setEmail("test@person1.com");

entityManager.persist(p1);

List<Person> list = entityManager.getEntityManager().createQuery("from Person").getResultList();

Assert.assertEquals(1L, list.size());

Person p2 = list.get(0);

Assert.assertEquals("test person 1", p2.getName());

Assert.assertEquals("test@person1.com", p2.getEmail());

Assert.assertEquals(p2.hashCode(), p2.hashCode());

Assert.assertTrue(p2.equals(p2));

}

}

持久层

持久层由Spring Boot自动管理。 PagingAndSortingRepository接口是CrudRepository的扩展,用于提供使用分页和排序抽象检索实体的附加方法。由于使用这些接口自然会使测试覆盖率达到100%,故无需写其他测试方法。

package com.myapp.sample.repositories;

import com.myapp.sample.model.Person;

import org.springframework.data.repository.PagingAndSortingRepository;

import org.springframework.data.rest.core.annotation.RestResource;

@RestResource(exported=false)

public interface PersonRepository extends PagingAndSortingRepository<Person, Long> {

}

业务层

PersonService接口包含三个操作:保存,按ID查找,以及查找Repository层支持的多个CRUD操作的所有实例。

package com.myapp.sample.service;

import com.myapp.sample.model.Person;

import java.util.List;

public interface PersonService {

public List<Person> getAll();

public Person save(Person p);

public Person findById(Long ids);

}

PersonService接口通过调用持久层实现的并未添加任何业务服务实现。由于Spring Boot的持久层测试范围已全部覆盖,因此无需单独测试业务业务。

package com.myapp.sample.service;

import java.util.ArrayList;

import java.util.List;

import java.util.Optional;

import com.myapp.sample.model.Person;

import com.myapp.sample.repositories.PersonRepository;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

@Service

public class PersonServiceImpl implements PersonService {

@Autowired

PersonRepository personRepository;

@Override

public List<Person> getAll() {

List<Person> personList = new ArrayList<>();

personRepository.findAll().forEach(personList::add);

return personList;

}

@Override

public Person save(Person p) {

return personRepository.save(p);

}

@Override

public Person findById(Long id) {

Optional<Person> dbPerson = personRepository.findById(id);

return dbPerson.orElse(null);

}

}

REST API 层

通过将调用业务层来间接调用持久层来暴露REST API。

package com.myapp.sample.controller;

import java.util.List;

import com.myapp.sample.model.Person;

import com.myapp.sample.service.PersonService;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.http.ResponseEntity;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.RestController;

@RestController

public class PersonController {

@Autowired

PersonService personService;

@PostMapping(path = "/api/person")

public ResponseEntity<Person> register(@RequestBody Person p) {

return ResponseEntity.ok(personService.save(p));

}

@GetMapping(path = "/api/person")

public ResponseEntity<List<Person>> getAllPersons() {

return ResponseEntity.ok(personService.getAll());

}

@GetMapping(path = "/api/person/{person-id}")

public ResponseEntity<Person> getPersonById(@PathVariable(name="person-id", required=true)Long personId) {

Person person = personService.findById(personId);

if (person != null) {

return ResponseEntity.ok(person);

}

return ResponseEntity.notFound().build();

}

}

使用Spring Boot自带测试框架测试 REST API 层

package com.myapp.sample.controller;

import com.myapp.sample.model.Person;

import com.myapp.sample.repositories.PersonRepository;

import org.junit.Assert;

import org.junit.Before;

import org.junit.Test;

import org.junit.runner.RunWith;

import org.mockito.Mock;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;

import org.springframework.boot.test.web.client.TestRestTemplate;

import org.springframework.core.ParameterizedTypeReference;

import org.springframework.http.*;

import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import org.springframework.test.web.servlet.MockMvc;

import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import java.util.List;

@RunWith(SpringJUnit4ClassRunner.class)

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)

public class PersonControllerTest {

MockMvc mockMvc;

@Mock

private PersonController personController;

@Autowired

private TestRestTemplate template;

@Autowired

PersonRepository personRepository;

@Before

public void setup() throws Exception {

mockMvc = MockMvcBuilders.standaloneSetup(personController).build();

personRepository.deleteAll();

}

@Test

public void testRegister() throws Exception {

HttpEntity<Object> person = getHttpEntity(

"{\"name\": \"test 1\", \"email\": \"test10000000000001@gmail.com\"}");

ResponseEntity<Person> response = template.postForEntity(

"/api/person", person, Person.class);

Assert.assertEquals("test 1", response.getBody().getName());

Assert.assertEquals(200,response.getStatusCode().value());

}

@Test

public void testGetAllPersons() throws Exception {

HttpEntity<Object> person = getHttpEntity(

"{\"name\": \"test 1\", \"email\": \"test10000000000001@gmail.com\"}");

ResponseEntity<Person> response = template.postForEntity(

"/api/person", person, Person.class);

ParameterizedTypeReference<List<Person>> responseType = new ParameterizedTypeReference<List<Person>>(){};

ResponseEntity<List<Person>> response2 = template.exchange("/api/person", HttpMethod.GET, null, responseType);

Assert.assertEquals(response2.getBody().size(), 1);

Assert.assertEquals("test 1", response2.getBody().get(0).getName());

Assert.assertEquals(200,response2.getStatusCode().value());

}

@Test

public void testGetPersonById() throws Exception {

HttpEntity<Object> person = getHttpEntity(

"{\"name\": \"test 1\", \"email\": \"test10000000000001@gmail.com\"}");

ResponseEntity<Person> response = template.postForEntity(

"/api/person", person, Person.class);

ParameterizedTypeReference<List<Person>> responseType = new ParameterizedTypeReference<List<Person>>(){};

ResponseEntity<List<Person>> response2 = template.exchange("/api/person", HttpMethod.GET, null, responseType);

Long id = response2.getBody().get(0).getId();

ResponseEntity<Person> response3 = template.getForEntity(

"/api/person/" + id, Person.class);

Assert.assertEquals("test 1", response3.getBody().getName());

Assert.assertEquals(200,response3.getStatusCode().value());

}

@Test

public void testGetPersonByNull() throws Exception {

ResponseEntity<Person> response3 = template.getForEntity(

"/api/person/1", Person.class);

Assert.assertEquals(404,response3.getStatusCode().value());

}

private HttpEntity<Object> getHttpEntity(Object body) {

HttpHeaders headers = new HttpHeaders();

headers.setContentType(MediaType.APPLICATION_JSON);

return new HttpEntity<Object>(body, headers);

}

}

以上这些基本上是用于开发微服务的Java代码。

所需软件

现在看下管理微服务所需的软件。

Java

在本文的例子中,使用Java8。由于Jenkins需要Java8,所以建议使用Java8。

Git

安装最新版本的Git。

Docker

安装最新版本的Docker。由于我有一台Windows8机器,所以我使用的是Docker Toolbox for Windows。

MySQL

使用以下命令安装MySQL 5.7 Docker镜像:

docker pull mysql
docker run -d --name mysql -e MYSQL_DATABASE=person -e MYSQL_ROOT_PASSWORD=<root_password> -p 3306:3306 mysql

在Docker中使用ip命令获取Docker实例的ip并替换demo程序中resources\application.properties的ip。

Jenkins

使用以下命令安装Jenkins Blue Ocean版本:

docker pull jenkinsci/blueocean

docker run -u root --rm -d -p 8080:8080 -p 50000:50000 -v jenkins-data:/var/jenkins_home -v /var/run/docker.sock:/var/run/docker.sock jenkinsci/blueocean

DevOps 流水线

现在我们来看看用于构建,部署和管理Git存储库的DevOps管道。在我们理解管道之前,在Git Flow上花几分钟是很重要的。

Git Flow

Git Flow是Git的分支模型。它主要由主分支(与生产代码并行),开发分支(开发的主要分支),发布分支(用于开发)和功能分支(供开发人员使用)组成。在开发人员完成代码之后,会为团队负责人创建一个pull请求,以检查并将代码合并到开发中。创建发布分支后,错误修复将进入此分支,并在代码稳定后再次合并以开发和管理。在此模型中,标签是从主分支创建的,用于发布到生产。
可以在此处查看可视化描述:https://datasift.github.io/gitflow/IntroducingGitFlow.html

JenkinsFile

有必要在Jenkins中创建一个多分支管道。这允许Jenkins自动处理Git Flow。当提供到Git存储库的链接时,Jenkins会自动选择JenkinsFile。将阶段配置为在签入时触发构建时可视化地查看管道。在此示例中,我们使用PMD,CheckStyle和FindBugs检查代码。欢迎您尝试更成熟的工具,如Sonar,代替PMD,CheckStyle和FindBugs。在管道设置中,我们在一个步骤中构建镜像,并在另一个步骤中运行镜像,以便在主分支中发生更改时更新测试环境容器。打tag时,将使用镜像tag名称更新生产环境,如1.0.0。欢迎您尝试将此示例设置为用于生产的不同Jenkins文件和用于生产的Docker文件,这在生成镜像后是必需的。

#!/usr/bin/env groovy

pipeline {

agent any

triggers {

pollSCM('*/15 * * * *')

}

options { disableConcurrentBuilds() }

stages {

stage('Permissions') {

steps {

sh 'chmod 775 *'

}

}

stage('Cleanup') {

steps {

sh './gradlew --no-daemon clean'

}

}

stage('Check Style, FindBugs, PMD') {

steps {

sh './gradlew --no-daemon checkstyleMain checkstyleTest findbugsMain findbugsTest pmdMain pmdTest cpdCheck'

}

post {

always {

step([

$class : 'FindBugsPublisher',

pattern : 'build/reports/findbugs/*.xml',

canRunOnFailed : true

])

step([

$class : 'PmdPublisher',

pattern : 'build/reports/pmd/*.xml',

canRunOnFailed : true

])

step([

$class: 'CheckStylePublisher',

pattern: 'build/reports/checkstyle/*.xml',

canRunOnFailed : true

])

}

}

}

stage('Test') {

steps {

sh './gradlew --no-daemon check'

}

post {

always {

junit 'build/test-results/test/*.xml'

}

}

}

stage('Build') {

steps {

sh './gradlew --no-daemon build'

}

}

stage('Update Docker UAT image') {

when { branch "master" }

steps {

sh '''

docker login -u "<userid>" -p "<password>"

docker build --no-cache -t person .

docker tag person:latest amritendudockerhub/person:latest

docker push amritendudockerhub/person:latest

docker rmi person:latest

'''

}

}

stage('Update UAT container') {

when { branch "master" }

steps {

sh '''

docker login -u "<userid>" -p "<password>"

docker pull amritendudockerhub/person:latest

docker stop person

docker rm person

docker run -p 9090:9090 --name person -t -d amritendudockerhub/person

docker rmi -f $(docker images -q --filter dangling=true)

'''

}

}

stage('Release Docker image') {

when { buildingTag() }

steps {

sh '''

docker login -u "<userid>" -p "<password>"

docker build --no-cache -t person .

docker tag person:latest amritendudockerhub/person:${TAG_NAME}

docker push amritendudockerhub/person:${TAG_NAME}

docker rmi $(docker images -f “dangling=true” -q)

'''

}

}

}

}

具体描述参见:https://jenkins.io/doc/tutorials/build-a-multibranch-pipeline-project/

结语

使用Kubernetes进行部署可以改进此示例。但可以使用Docker创建一个完整的管道,但这不是本文的目标。欢迎大家提出更多好方案。

浩子淘天下 wechat
扫码关注我
鼓励原创